package com.csii.oauth.config;

import com.csii.core.constant.CommonConstant;
import com.csii.oauth.exception.AuthExceptionHandler;
import com.csii.oauth.exception.ExceptionOutput;
import com.csii.oauth.filter.TokenAuthenticationFilter;
import com.csii.oauth.filter.exchange.IAuthExchange;
import com.csii.oauth.filter.exchange.impl.HeaderParseExchange;
import com.csii.oauth.filter.exchange.impl.MocAuthExchange;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.security.oauth2.provider.authentication.TokenExtractor;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;

/**
 * @Description TODO
 * @Date 2020/7/3 12:43
 * @Created by Ian
 * @Contact: 19107614@qq.com
 */
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(MocLoginUserProperties.class)
@EnableResourceServer
//设置方法开启权限访问
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
@Import(AuthExceptionHandler.class)
public class AppResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Value("${security.oauth2.resource.id:'user_resource'}")
    private String resource_id;
    @Value("${security.oauth2.resource.match-url:}")
    private String matchUrl;
    @Value("${security.oauth2.resource.miss-url:}")
    private String missUrl;
    @Value("${authorization.check-token-url:}")
    private String checkTokenUrl;
    @Value("${authorization.clientId:'local9001'}")
    private String clientId;
    @Value("${authorization.clientSecret:'admin123'}")
    private String clientSecret;
    @Autowired
    private ExceptionOutput exceptionOutput;
    @Autowired
    private IAuthExchange authExchange;


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        // 当前资源服务器的资源id，认证服务会认证客户端有没有访问这个资源id的权限，有则可以访问当前服务
        resources.authenticationEntryPoint(authenticationEntryPoint())
                .accessDeniedHandler(customAccessDeniedHandler());
        resources.resourceId(resource_id);
        if (!StringUtils.isEmpty(checkTokenUrl)) {
            resources.tokenExtractor(new TokenExtractor() {

                public Authentication extract(HttpServletRequest request) {
                    String tokenValue = this.extractToken(request);
                    if (tokenValue != null) {
                        PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
                        return authentication;
                    } else {
                        return null;
                    }
                }

                protected String extractToken(HttpServletRequest request) {
                    String token = this.extractHeaderToken(request);
                    if (token == null) {
                        log.debug("Token not found in headers. Trying request parameters.");
                        token = request.getParameter("access_token");
                        if (token == null) {
                            log.debug("Token not found in request parameters.  Not an OAuth2 request.");
                        } else {
                            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer");
                        }
                    }

                    return token;
                }

                protected String extractHeaderToken(HttpServletRequest request) {
                    Enumeration headers = request.getHeaders("Authorization");

                    String value;
                    do {
                        if (!headers.hasMoreElements()) {
                            String authHeaderValue = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
                            if (StringUtils.isEmpty(authHeaderValue))
                                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, authHeaderValue);
                            return authHeaderValue;
                        }

                        value = (String) headers.nextElement();
                    } while (!value.toLowerCase().startsWith("Bearer".toLowerCase()));

                    String authHeaderValue = value.substring("Bearer".length()).trim();
                    request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim());
                    int commaIndex = authHeaderValue.indexOf(44);
                    if (commaIndex > 0) {
                        authHeaderValue = authHeaderValue.substring(0, commaIndex);
                    }
                    return authHeaderValue;
                }
            });
            resources.tokenServices(remoteTokenService());
        }
    }

    @Bean
    @Primary
    @ConditionalOnProperty("authorization.check-token-url")
    public ResourceServerTokenServices remoteTokenService() {
        // 远程认证服务器进行校验 token 是否有效
        RemoteTokenServices service = new RemoteTokenServices();
        // 请求认证服务器校验的地址，默认情况 这个地址在认证服务器它是拒绝访问，要设置为认证通过可访问
        service.setCheckTokenEndpointUrl(checkTokenUrl);
        service.setClientId(clientId);
        service.setClientSecret(clientSecret);
        DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
        defaultAccessTokenConverter.setUserTokenConverter(new UserAuthenticationConverter() {

            private Collection<? extends GrantedAuthority> defaultAuthorities;

            public Map<String, ?> convertUserAuthentication(Authentication authentication) {
                Map<String, Object> response = new LinkedHashMap();
                response.put("user_name", authentication.getName());
                if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
                    response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
                }

                return response;
            }

            public Authentication extractAuthentication(Map<String, ?> map) {
                if (map.containsKey("user_name")) {
                    Collection<? extends GrantedAuthority> authorities = this.getAuthorities(map);
                    map.remove("authorities");
                    Object principal = map;
                    return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
                } else {
                    return null;
                }
            }

            private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
                if (!map.containsKey("authorities")) {
                    return this.defaultAuthorities;
                } else {
                    Object authorities = map.get("authorities");
                    if (authorities instanceof String) {
                        return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
                    } else if (authorities instanceof Collection) {
                        return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection) authorities));
                    } else {
                        throw new IllegalArgumentException("Authorities must be either a String or a Collection");
                    }
                }
            }
        });
        service.setAccessTokenConverter(defaultAccessTokenConverter);
        return service;
    }

    /*@ConditionalOnBean(TokenStore.class)
    @Bean
    public ResourceServerTokenServices localTokenService(@Autowired TokenStore tokenStore){
        DefaultTokenServices defaultTokenServices= new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore);
        return defaultTokenServices;
    }*/
    @Override
    public void configure(HttpSecurity http) throws Exception {
        List<String> urls = new ArrayList<>(Arrays.asList(new String[]{"/doc.html**", "/api-docs-ext**", "/swagger-resources**", "/api-docs**", "/swagger-ui.html**", "/webjars/**", "/swagger-resources/**",
                "/v2/api-docs-ext**", "/v2/api-docs**", "/favicon.ico", "/oauthConfig", "/token/**", "/login/**", "/**.html", "/css/**", "/js/**", "/actuator/**"}));
        if (!StringUtils.isEmpty(missUrl))
            urls.addAll(Arrays.asList(missUrl.split(",")));
        http.headers().frameOptions().disable();
        if (StringUtils.isEmpty(matchUrl)) {
            http.antMatcher("nonAuth").authorizeRequests().anyRequest().permitAll();
            return;
        }
        http.antMatcher(matchUrl).sessionManagement()
                // SpringSecurity不会使用也不会创建HttpSession实例
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 授权规则配置，也可以再方法上配置
                //.antMatchers("/product/list").hasAuthority("product:list")
                // 所有请求，都需要有all范围（scope）
//                .antMatchers("/**").access("#oauth2.hasScope('all')")
                // 等价于上面
//                .anyRequest().access("#oauth2.hasScope('all')")
                .antMatchers(urls.toArray(new String[urls.size()])).permitAll()
                .anyRequest().authenticated();
        http.addFilterAfter(tokenAuthenticationFilter(authExchange), AnonymousAuthenticationFilter.class);

    }

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response,
                                 AuthenticationException authException) throws ServletException {
                Map<String, Object> map = new HashMap<String, Object>();
                Throwable cause = authException.getCause();
                ResponseEntity entity = exceptionOutput.outputAuthException(authException);
                response.setStatus(entity.getStatusCode().value());
                response.setStatus(entity.getStatusCodeValue());
                response.setHeader("Content-Type", "application/json;charset=UTF-8");
                try {
                    response.getWriter().write(entity.getBody().toString());
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
        };

    }

    @Bean
    public AccessDeniedHandler customAccessDeniedHandler() {
        return new AccessDeniedHandler() {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response,
                               AccessDeniedException accessDeniedException)
                    throws IOException, ServletException {
                ResponseEntity entity = exceptionOutput.outputAuthException(accessDeniedException);
                response.setStatus(entity.getStatusCode().value());
                response.setHeader("Content-Type", "application/json;charset=UTF-8");
                try {
                    response.getWriter().write(entity.getBody().toString());
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }

            }
        };

    }

    @Bean
    @ConditionalOnMissingBean
    public IAuthExchange headerAuthExchange() {
        return new HeaderParseExchange();
    }

    @Bean
    @ConditionalOnProperty(prefix = "cola.moc.loginuser", name = "username")
    @Primary
    public IAuthExchange mocAuthExchange(@Autowired MocLoginUserProperties properties) {
        return new MocAuthExchange(properties);
    }

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter(IAuthExchange authExchange) {
        return new TokenAuthenticationFilter(authExchange);
    }

    @Bean
    @Primary
    public RestTemplate getRestTemplate(RestTemplateBuilder builder) {
        builder.setReadTimeout(Duration.ofSeconds(5L));
        builder.setConnectTimeout(Duration.ofSeconds(5L));
        RestTemplate restTemplate = builder.build();
        // 配置RestTemplate连接池
        restTemplate.setRequestFactory(getHttpRequestFactory());
        restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            assert attributes != null;
            HttpServletRequest req = attributes.getRequest();
            String token = req.getHeader(CommonConstant.X_ACCESS_TOKEN);
            if (!StringUtils.isEmpty(token))
                //代码扫描
                token = token.replaceAll("(\r\n|\r|\n|\n\r)","");
                request.getHeaders().add(CommonConstant.X_ACCESS_TOKEN, token);
            request.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
            request.getHeaders().add("Accept", MediaType.APPLICATION_JSON.toString());
            return execution.execute(request, body);
        }));
        // 使用 utf-8 编码集的 conver 替换默认的 conver（默认的 string conver 的编码集为 "ISO-8859-1"）
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        messageConverters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
        messageConverters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }

    @Bean
    public ClientHttpRequestFactory getHttpRequestFactory() {
        return new HttpComponentsClientHttpRequestFactory(getHttpClient());
    }

    @Bean
    public HttpClient getHttpClient() {
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", SSLConnectionSocketFactory.getSocketFactory())
                .build();

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
        // 设置整个连接池最大连接数
        connectionManager.setMaxTotal(100);
        // 同路由并发数（每个主机的并发）
        connectionManager.setDefaultMaxPerRoute(50);

        RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(30000)  //返回数据的超时时间
                .setConnectTimeout(10000) //连接上服务器的超时时间
                .setConnectionRequestTimeout(1000) //从连接池中获取连接的超时时间
                .build();
        return HttpClientBuilder.create()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(connectionManager)
                .build();
    }
}
